/**************************************************************************
* This file is part of Hawkengine.
*
* Hawkengine is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Hawkengine is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Hawkengine.  If not, see <http://www.gnu.org/licenses/>.
**************************************************************************/


/**************************************************************************
* Dependencies
**************************************************************************/
#include "engine/common.h"
#include "engine/image.h"
#include "lodepng.h"

/**************************************************************************
* Preprocessing
**************************************************************************/
#define MAX_PIXEL_DATA 65535 // max USHORT (2^16 - 1)
#define PIXEL_DATA_TYPE USHORT

/**************************************************************************
* Types
**************************************************************************/

/**************************************************************************
* Variables
**************************************************************************/

/**************************************************************************
* Static Variables
**************************************************************************/
static const UCHAR EMPTY_PIXEL[4] = {0, 0, 0, 0};
static LodePNG_Decoder png_decoder;
static LodePNG_Encoder png_encoder;

/**************************************************************************
* Function Prototypes
**************************************************************************/
static void add_alpha(image_type* image, const color_type* transparent_color);
static void ensure_allocation(image_type* image, image_data_type image_data);
static void ensure_data(image_type* image, image_data_type image_data);
static int get_data_size(const image_type* image, image_data_type image_data);
static PIXEL_DATA_TYPE luminance(const UCHAR* rgb);
static BOOL parse_bmp_file(const file_data_type* data, image_type* image);
static BOOL parse_png_file(const file_data_type* data, image_type* image);
static UCHAR* pixel(const image_type* image, image_data_type image_data,
                    int x, int y);
static PIXEL_DATA_TYPE pixel_gradient_magnitude(const image_type* image,
                                                image_data_type image_data,
                                                int x, int y);
static PIXEL_DATA_TYPE pixel_gradient_x(const image_type* image,
                                        image_data_type image_data, int x, int y);
static PIXEL_DATA_TYPE pixel_gradient_y(const image_type* image,
                                        image_data_type image_data, int x, int y);

/**************************************************************************
* Function Implementations
**************************************************************************/
static void add_alpha(image_type* image, const color_type* transparent_color)
{
  UCHAR* old_data;
  int old_channels;
  int i;

  if ((image->channels >= 4) || (image->channels < 3)) {
    return;
  }

  // save off data without alpha
  old_channels = image->channels;
  old_data = image->data[IMAGE_DATA_CHANNELS];
  image->data[IMAGE_DATA_CHANNELS] = NULL;

  image->channels = 4;
  ensure_allocation(image, IMAGE_DATA_CHANNELS);
 
  // add alpha channel and reverse to make image white on black
  for (i = 0; i < (image->width * image->height); i++) {
    memcpy(&(image->data[IMAGE_DATA_CHANNELS][image->channels * i]),
           &(old_data[old_channels * i]), (old_channels * sizeof(UCHAR)));
    if ((old_data[old_channels * i] == transparent_color->r) &&
        (old_data[(old_channels * i) + 1] == transparent_color->g) &&
        (old_data[(old_channels * i) + 2] == transparent_color->b)) {
      image->data[IMAGE_DATA_CHANNELS][(image->channels * i) + 3] = 0;
    } else {
      // full alpha
      image->data[IMAGE_DATA_CHANNELS][(image->channels * i) + 3] = 255;
    }
  }

  // clean up old data
  free(old_data);
}
static void ensure_allocation(image_type* image, image_data_type image_data)
{
  int size;

  if (image->data[image_data]) {
    return;
  }
  size = (get_data_size(image, image_data) * image->width * image->height);
  image->data[image_data] = malloc(size);
  memset(image->data[image_data], 0, size);
}

static void ensure_data(image_type* image, image_data_type image_data)
{
  if (!(image->data[image_data])) {
    image_calculate(image, image_data);
  }
}

static int get_data_size(const image_type* image, image_data_type image_data)
{
  int size = 0;

  switch (image_data) {
  case IMAGE_DATA_CHANNELS:
    size = (image->channels * sizeof(UCHAR));
    break;
  case IMAGE_DATA_COUNT:
    {
      int i;
      for (i = 0; i < IMAGE_DATA_COUNT; i++) {
        size += get_data_size(image, i);
      }
    }
    break;
  default:
    size = sizeof(PIXEL_DATA_TYPE);
    break;
  }

  return size;
}

void image_add_seam(image_type* image, BOOL horizontal)
{
  UCHAR pixel1[4], pixel2[4];
  UCHAR* new_pixel;
  image_type new_image;
  int i, x, y, old_x, old_y, pixel_size;

  if ((image->width < 2) || (image->height < 2)) {
    return;
  }

  pixel_size = get_data_size(image, IMAGE_DATA_CHANNELS);

  new_image = *image;
  if (horizontal) {
    new_image.height += 1;
  } else {
    new_image.width += 1;
  }
  for (i = 0; i < IMAGE_DATA_COUNT; i++) {
    new_image.data[i] = NULL;
  }

  ensure_allocation(&new_image, IMAGE_DATA_CHANNELS);

  if (horizontal) {
    ensure_data(image, IMAGE_DATA_SEAM_H);
  } else {
    ensure_data(image, IMAGE_DATA_SEAM_V);
  }

  old_x = old_y = 0;
  if (horizontal) {
    // horizontal seams
    for (x = 0; x < new_image.width; x++) {
      old_y = 0;
      for (y = 0; y < new_image.height; y++) {
        memcpy(pixel(&new_image, IMAGE_DATA_CHANNELS, x, y),
               pixel(image, IMAGE_DATA_CHANNELS, x, old_y),
               pixel_size);
        if (*((PIXEL_DATA_TYPE*)pixel(image, IMAGE_DATA_SEAM_H, x, old_y)) > 0) {
          // also write the next pixel
          new_pixel = pixel(&new_image, IMAGE_DATA_CHANNELS, x, y+1);
          memcpy(pixel1, pixel(image, IMAGE_DATA_CHANNELS, x, old_y), pixel_size);
          if (old_y > 0) {
            // averate with previous pixel in column
            memcpy(pixel2, pixel(image, IMAGE_DATA_CHANNELS, x, old_y-1), pixel_size);
          } else {
            // average with next pixel in column
            memcpy(pixel2, pixel(image, IMAGE_DATA_CHANNELS, x, old_y+1), pixel_size);
          }
          for (i = 0; i < pixel_size; i++) {
            new_pixel[i] = ((pixel1[i] + pixel2[i]) / 2);
          }
          y++;
        }
        old_y++;
      }
    }
  } else {
    // vertical seams
    for (y = 0; y < new_image.height; y++) {
      old_x = 0;
      for (x = 0; x < new_image.width; x++) {
        memcpy(pixel(&new_image, IMAGE_DATA_CHANNELS, x, y),
               pixel(image, IMAGE_DATA_CHANNELS, old_x, y),
               pixel_size);
        if (*((PIXEL_DATA_TYPE*)pixel(image, IMAGE_DATA_SEAM_V, old_x, y)) > 0) {
          // also write the next pixel
          new_pixel = pixel(&new_image, IMAGE_DATA_CHANNELS, x+1, y);
          memcpy(pixel1, pixel(image, IMAGE_DATA_CHANNELS, old_x, y), pixel_size);
          if (old_x > 0) {
            // average the previous pixel in row
            memcpy(pixel2, pixel(image, IMAGE_DATA_CHANNELS, old_x-1, y), pixel_size);
          } else {
            // average with next pixel in row
            memcpy(pixel2, pixel(image, IMAGE_DATA_CHANNELS, old_x+1, y), pixel_size);
          }
          for (i = 0; i < pixel_size; i++) {
            new_pixel[i] = ((pixel1[i] + pixel2[i]) / 2);
          }
          x++;
        }
        old_x++;
      }
    }
  }
 
  // update the original image and clean up old data
  for (i = 0; i < IMAGE_DATA_COUNT; i++) {
    free(image->data[i]);
  }
  *image = new_image;
}

void image_calculate(image_type* image, image_data_type image_data)
{
  int x, y, data_size;

  ensure_allocation(image, image_data);

  if (image_data == IMAGE_DATA_CHANNELS) {
    return;
  }

  data_size = get_data_size(image, image_data);

  switch (image_data) {
  case IMAGE_DATA_INTENSITY:
    {
      for (x = 0; x < image->width; x++) {
        for (y = 0; y < image->height; y++) {
          pixel(image, image_data, x, y)[0] =
            luminance(pixel(image, IMAGE_DATA_CHANNELS, x, y));
        }
      }
    }
    break;
  case IMAGE_DATA_EDGE:
    {
      ensure_data(image, IMAGE_DATA_INTENSITY);

      for (x = 0; x < image->width; x++) {
        for (y = 0; y < image->height; y++) {
          pixel(image, image_data, x, y)[0] =
            pixel_gradient_magnitude(image, IMAGE_DATA_INTENSITY, x, y);
        }
      }
    }
    break;
  case IMAGE_DATA_ENERGY_H:
    {
      PIXEL_DATA_TYPE energy;

      ensure_data(image, IMAGE_DATA_EDGE);

      // energy flows left
      for (x = (image->width - 1); x >= 0; x--) {
        for (y = 0; y < image->height; y++) {
          energy = *((PIXEL_DATA_TYPE*)pixel(image, IMAGE_DATA_ENERGY_H, (x + 1), (y - 1)));
          _UPDATE_MIN(energy, *((PIXEL_DATA_TYPE*)pixel(image, IMAGE_DATA_ENERGY_H, (x + 1), y)));
          _UPDATE_MIN(energy, *((PIXEL_DATA_TYPE*)pixel(image, IMAGE_DATA_ENERGY_H, (x + 1), (y + 1))));
          _ADD(energy, *((PIXEL_DATA_TYPE*)pixel(image, IMAGE_DATA_EDGE, x, y)), MAX_PIXEL_DATA);
          memcpy(pixel(image, image_data, x, y), &energy, data_size);
        }
      }
    }
    break;
  case IMAGE_DATA_ENERGY_V:
    {
      PIXEL_DATA_TYPE energy;
 
      ensure_data(image, IMAGE_DATA_EDGE);

      // energy flows up
      for (y = (image->height - 1); y >= 0; y--) {
        for (x = 0; x < image->width; x++) {
          energy = *((PIXEL_DATA_TYPE*)pixel(image, IMAGE_DATA_ENERGY_V, (x - 1), (y + 1)));
          _UPDATE_MIN(energy, *((PIXEL_DATA_TYPE*)pixel(image, IMAGE_DATA_ENERGY_V, x, (y + 1))));
          _UPDATE_MIN(energy, *((PIXEL_DATA_TYPE*)pixel(image, IMAGE_DATA_ENERGY_V, (x + 1), (y + 1))));
          _ADD(energy, *((PIXEL_DATA_TYPE*)pixel(image, IMAGE_DATA_EDGE, x, y)), MAX_PIXEL_DATA);
          memcpy(pixel(image, image_data, x, y), &energy, data_size);
        }
      }
    }
    break;
  case IMAGE_DATA_SEAM_H:
    {
      PIXEL_DATA_TYPE seam_value;
      PIXEL_DATA_TYPE* value;
      int seam_idx, min_seam_idx, max_seam_idx;

      // start seam from left
      ensure_data(image, IMAGE_DATA_ENERGY_H);

      seam_idx = 0;
      for (x = 0; x < image->width; x++) {
        seam_value = MAX_PIXEL_DATA;

        if (x == 0) {
          // first column
          seam_idx = 0;
          min_seam_idx = 0;
          max_seam_idx = (image->height - 1);
        } else {
          // not first column
          min_seam_idx = _MAX((seam_idx - 1), 0);
          max_seam_idx = _MIN((seam_idx + 1), (image->height - 1));
        }
 
        for (y = min_seam_idx; y <= max_seam_idx; y++) {
          value = (PIXEL_DATA_TYPE*)pixel(image, IMAGE_DATA_ENERGY_H, x, y);
          if (*value < seam_value) {
            seam_idx = y;
            seam_value = *value;
          }
        }

        *((PIXEL_DATA_TYPE*)pixel(image, image_data, x, seam_idx)) = MAX_PIXEL_DATA;
      }
    }
    break;
  case IMAGE_DATA_SEAM_V:
    {
      PIXEL_DATA_TYPE seam_value;
      PIXEL_DATA_TYPE* value;
      int seam_idx, min_seam_idx, max_seam_idx;

      // start seam from top
      ensure_data(image, IMAGE_DATA_ENERGY_V);

      seam_idx = 0;
      for (y = 0; y < image->height; y++) {
        seam_value = MAX_PIXEL_DATA;

        if (y == 0) {
          // first row
          seam_idx = 0;
          min_seam_idx = 0;
          max_seam_idx = (image->width - 1);
        } else {
          // not first row
          min_seam_idx = _MAX((seam_idx - 1), 0);
          max_seam_idx = _MIN((seam_idx + 1), (image->width - 1));;
        }
 
        for (x = min_seam_idx; x <= max_seam_idx; x++) {
          value = (PIXEL_DATA_TYPE*)pixel(image, IMAGE_DATA_ENERGY_V, x, y);
          if (*value < seam_value) {
            seam_idx = x;
            seam_value = *value;
          }
        }

        *((PIXEL_DATA_TYPE*)pixel(image, image_data, seam_idx, y)) = MAX_PIXEL_DATA;
      }
    }
    break;
  default:
    break;
  }
}

void image_clone(const image_type* image, image_type* new_image)
{
  int i;

  *new_image = *image;

  for (i = 0; i < IMAGE_DATA_COUNT; i++) {
    new_image->data[i] = NULL;
    ensure_allocation(new_image, i);
    memcpy(new_image->data[i], image->data[i],
           (get_data_size(new_image, i) * new_image->width * new_image->height));
  }
}

void image_create(int width, int height, int channels, image_type* image)
{
  memset(&image, 0, sizeof(image));
  image->width = width;
  image->height = height;
  image->channels = channels;

  ensure_allocation(image, IMAGE_DATA_CHANNELS);
}

BOOL image_create_from_file(const file_data_type* file_data, image_type* image)
{
  memset(image, 0, sizeof(image_type));
  if (strcmp(file_data->extension, "bmp") == 0) {
    return parse_bmp_file(file_data, image);
  } else if (strcmp(file_data->extension, "png") == 0) {
    return parse_png_file(file_data, image);
  }

  return FALSE;
}

void image_destroy(image_type* image)
{
  int i;

  for (i = 0; i < IMAGE_DATA_COUNT; i++) {
    free(image->data[i]);
  }
  memset(image, 0, sizeof(image_type));
}

void image_flip(image_type* image, BOOL horizontal)
{
  int pixel_size;
  UCHAR temp[4];
  int i, x, y;
 
  for (i = 0; i < IMAGE_DATA_COUNT; i++) {
    if (!(image->data[i])) {
      continue;
    }
    pixel_size = get_data_size(image, i);
    if (horizontal) {
      for (y = 0; y < image->height; y++) {
        for (x = 0; x < (image->width / 2); x++) {
          memcpy(temp, pixel(image, i, x, y), pixel_size);
          memcpy(pixel(image, i, x, y),
                 pixel(image, i, (image->width - 1 - x), y), pixel_size);
          memcpy(pixel(image, i, (image->width - 1 - x), y), temp, pixel_size);
        }
      }
    } else {
      for (x = 0; x < image->width; x++) {
        for (y = 0; y < (image->height / 2); y++) {
          memcpy(temp, pixel(image, i, x, y), pixel_size);
          memcpy(pixel(image, i, x, y),
                 pixel(image, i, x, (image->height - 1 - y)), pixel_size);
          memcpy(pixel(image, i, x, (image->height - 1 - y)), temp, pixel_size);
        }
      }
    }
  }
}

void image_init(void)
{
  LodePNG_Decoder_init(&png_decoder);
  LodePNG_Encoder_init(&png_encoder);
}

void image_remove_seam(image_type* image, BOOL horizontal)
{
  image_type new_image;
  int i, x, y, old_x, old_y, pixel_size;

  if ((image->width < 2) || (image->height < 2)) {
    return;
  }

  pixel_size = get_data_size(image, IMAGE_DATA_CHANNELS);

  new_image = *image;
  if (horizontal) {
    new_image.height -= 1;
  } else {
    new_image.width -= 1;
  }
  for (i = 0; i < IMAGE_DATA_COUNT; i++) {
    new_image.data[i] = NULL;
  }

  ensure_allocation(&new_image, IMAGE_DATA_CHANNELS);

  if (horizontal) {
    ensure_data(image, IMAGE_DATA_SEAM_H);
  } else {
    ensure_data(image, IMAGE_DATA_SEAM_V);
  }

  old_x = old_y = 0;
  if (horizontal) {
    // horizontal seams
    for (x = 0; x < new_image.width; x++) {
      old_y = 0;
      for (y = 0; y < new_image.height; y++) {
        if (*((PIXEL_DATA_TYPE*)pixel(image, IMAGE_DATA_SEAM_H, x, old_y)) > 0) {
          // skip this guy
          old_y++;
        }
        memcpy(pixel(&new_image, IMAGE_DATA_CHANNELS, x, y),
               pixel(image, IMAGE_DATA_CHANNELS, x, old_y),
               pixel_size);

        old_y++;
      }
    }
  } else {
    // vertical seams
    for (y = 0; y < new_image.height; y++) {
      old_x = 0;
      for (x = 0; x < new_image.width; x++) {
        if (*((PIXEL_DATA_TYPE*)pixel(image, IMAGE_DATA_SEAM_V, old_x, y)) > 0) {
          // skip this guy
          old_x++;
        }
        memcpy(pixel(&new_image, IMAGE_DATA_CHANNELS, x, y),
               pixel(image, IMAGE_DATA_CHANNELS, old_x, y),
               pixel_size);

        old_x++;
      }
    }
  }
 
  // update the original image and clean up old data
  for (i = 0; i < IMAGE_DATA_COUNT; i++) {
    free(image->data[i]);
  }
  *image = new_image;
}

void image_resize(image_type* image, int width, int height)
{
#define ADD_PIXEL_PORTION(dest_pixel, channel, src_pixel_x, src_pixel_y, portion) (\
    dest_pixel[channel] += ((pixel(image, IMAGE_DATA_CHANNELS, \
                                   src_pixel_x, src_pixel_y)[channel]) * portion) \
  )

  image_type new_image;
  UCHAR* new_pixel;
  int x, y, old_x, old_y, pixel_size;
  float dx, dy, portion_x, portion_y;
  int i;

  if ((width < 1) || (height < 1) || (image->width < 1) || (image->height < 1)) {
    return;
  }

  new_image = *image;
  new_image.width = width;
  new_image.height = height;
  for (i = 0; i < IMAGE_DATA_COUNT; i++) {
    new_image.data[i] = NULL;
  }
 
  pixel_size = get_data_size(&new_image, IMAGE_DATA_CHANNELS);

  ensure_allocation(&new_image, IMAGE_DATA_CHANNELS);

  dx = (new_image.width / (float)image->width);
  dy = (new_image.height / (float)image->height);

  // create new image using bilinear interpolation
  for (x = 0; x < new_image.width; x++) {
    for (y = 0; y < new_image.height; y++) {
      old_x = (int)(x / dx);
      old_y = (int)(y / dy);

      portion_x = ((x / dx) - old_x);
      portion_y = ((y / dy) - old_y);

      new_pixel = pixel(&new_image, IMAGE_DATA_CHANNELS, x, y);
      memset(new_pixel, 0, pixel_size);
      for (i = 0; i < new_image.channels; i++) {
        ADD_PIXEL_PORTION(new_pixel, i, old_x, old_y, (portion_x * portion_y));
        ADD_PIXEL_PORTION(new_pixel, i, (old_x + 1), old_y,
                          ((1 - portion_x) * portion_y));
        ADD_PIXEL_PORTION(new_pixel, i, old_x, (old_y + 1),
                          (portion_x * (1 - portion_y)));
        ADD_PIXEL_PORTION(new_pixel, i, (old_x + 1), (old_y + 1),
                          ((1 - portion_x) * (1 - portion_y)));
      }
    }
  }
 
  // update the original image and clean up old data
  for (i = 0; i < IMAGE_DATA_COUNT; i++) {
    free(image->data[i]);
  }
  *image = new_image;

#undef ADD_PIXEL_PORTION
}

UINT image_texture(const image_type* image, image_data_type image_data)
{
  UINT texture = 0;
  int format;

  switch (get_data_size(image, image_data)) {
  case 1:
    format = GL_LUMINANCE;
    break;
  case 2:
    format = GL_LUMINANCE_ALPHA;
    break;
  case 3:
    format = GL_RGB;
    break;
  case 4:
    format = GL_RGBA;
    break;
  default:
    return texture;
  }

  glEnable(GL_TEXTURE_2D);
  glGenTextures(1, &texture);
  glBindTexture(GL_TEXTURE_2D, texture);

  glTexImage2D(GL_TEXTURE_2D, 0, format, image->width, image->height, 0,
               format, GL_UNSIGNED_BYTE, image->data[image_data]);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
  glDisable(GL_TEXTURE_2D);

  return texture;

}

UINT image_texture_from_data(const file_data_type* file_data)
{
  UINT texture = 0;
  image_type image;

  if (image_create_from_file(file_data, &image)) {
    texture = image_texture(&image, IMAGE_DATA_CHANNELS);
  }

  image_destroy(&image);

  return texture;
}

UINT image_texture_from_data_add_alpha(const file_data_type* file_data,
                                       const color_type* transparent_color)
{
  UINT texture = 0;
  image_type image;
 
  if (image_create_from_file(file_data, &image)) {
    if (image.channels == 3) {
      // add in alpha
      add_alpha(&image, transparent_color);
    }
    // already has alpha
    texture = image_texture(&image, IMAGE_DATA_CHANNELS);
  }
 
  image_destroy(&image);

  return texture;
}

void image_uninit(void)
{
  LodePNG_Decoder_cleanup(&png_decoder);
  LodePNG_Encoder_cleanup(&png_encoder);
}
BOOL image_write(image_type* image, image_data_type image_data,
                 const char* type, UCHAR** file_data, int* file_size)
{
  int bit_depth, channels;
  BOOL ok = FALSE;
  (*file_data) = NULL;

  // create the desired data if we need to
  ensure_data(image, image_data);

  bit_depth = 8;
  channels = get_data_size(image, image_data);

  switch (image_data) {
  case IMAGE_DATA_CHANNELS:
    break;
  default:
    if (channels > 1) {
      bit_depth *= channels;
      channels = 1;
    }
  }

  if (strcmp(type, "png") == 0) {
    size_t size = 0;
    png_encoder.settings.zlibsettings.windowSize = 2048;
    png_encoder.settings.autoLeaveOutAlphaChannel = FALSE;
    png_encoder.infoRaw.color.bitDepth = bit_depth;
    switch (channels) {
    case 1:
      // grey
      png_encoder.infoRaw.color.colorType = 0;
      break;
    case 2:
      // grey + alpha
      png_encoder.infoRaw.color.colorType = 4;
      break;
    case 3:
      // RGB
      png_encoder.infoRaw.color.colorType = 2;
      break;
    case 4:
      // RGBA
      png_encoder.infoRaw.color.colorType = 6;
      break;
    default:
      return FALSE;
    }
    png_encoder.infoPng.color = png_encoder.infoRaw.color;
    LodePNG_encode(&png_encoder, file_data, &size, image->data[image_data],
                   image->width, image->height);
    (*file_size) = (int)size;
    ok = !(png_encoder.error);
  }

  return (ok && ((*file_data) != NULL) && ((*file_size) > 0));
}

static PIXEL_DATA_TYPE luminance(const UCHAR* rgb)
{
  return (PIXEL_DATA_TYPE)((int)((0.299 * rgb[0]) + (0.587 * rgb[1]) + (0.114 * rgb[2])));
}

static BOOL parse_bmp_file(const file_data_type* file_data, image_type* image)
{
  UINT img_data_offset;
  USHORT bits_per_pixel;
  UINT compression_type;
  int i;
  UCHAR tmp;

  memset(image, 0, sizeof(image_type));

  // read image parameters
  memcpy(&img_data_offset, &(file_data->data[10]), sizeof(img_data_offset));
  memcpy(&(image->width), &(file_data->data[18]), sizeof(image->width));
  memcpy(&(image->height), &(file_data->data[22]), sizeof(image->height));
 
  memcpy(&bits_per_pixel, &(file_data->data[28]), sizeof(bits_per_pixel));
  if(bits_per_pixel != 24) {
    return FALSE;
  }
  image->channels = 3;
 
  memcpy(&compression_type, &(file_data->data[30]), sizeof(compression_type));
  if(compression_type) {
    return FALSE;
  }

  // move the pointer to the start of data
  ensure_allocation(image, IMAGE_DATA_CHANNELS);
  memcpy(image->data[IMAGE_DATA_CHANNELS], &(file_data->data[img_data_offset]),
         image->width * image->height * get_data_size(image, IMAGE_DATA_CHANNELS));
 
  // rearrange the data
  for (i = 0; i < (image->width * image->height); i++) {
    tmp = image->data[IMAGE_DATA_CHANNELS][3*i];
    image->data[IMAGE_DATA_CHANNELS][3*i] = image->data[IMAGE_DATA_CHANNELS][3*i+2];
    image->data[IMAGE_DATA_CHANNELS][3*i+2] = tmp;
  }

  return TRUE;
}

static BOOL parse_png_file(const file_data_type* file_data, image_type* image)
{
  size_t size;

  memset(image, 0, sizeof(image_type));

  LodePNG_decode(&png_decoder, &(image->data[IMAGE_DATA_CHANNELS]), &size,
                 file_data->data, file_data->size);
  if (png_decoder.error || !(image->data)) {
    return FALSE;
  }

  image->width = png_decoder.infoPng.width;
  image->height = png_decoder.infoPng.height;

  switch (png_decoder.infoPng.color.colorType) {
  case 0:
  case 3:
    // grey
    image->channels = 1;
    break;
  case 4:
    // grey + alpha
    image->channels = 2;
    break;
  case 2:
    // RGB
    image->channels = 3;
    break;
  case 6:
    // RGBA
    image->channels = 4;
    break;
  default:
    return FALSE;
  }
 
  image_flip(image, FALSE);

  return TRUE;
}

static UCHAR* pixel(const image_type* image, image_data_type image_data,
                    int x, int y)
{
  static UCHAR empty_pixel[4] = {0, 0, 0, 0};
  if ((x < 0) || (x >= image->width) || (y < 0) || (y >= image->height)) {
    memcpy(&empty_pixel, &EMPTY_PIXEL, sizeof(empty_pixel));
    return empty_pixel;
  } else {
    return (image->data[image_data] + (((y * image->width) + x) *
                                       get_data_size(image, image_data)));
  }
}

static PIXEL_DATA_TYPE pixel_gradient_magnitude(const image_type* image,
                                                image_data_type image_data,
                                                int x, int y)
{
  static PIXEL_DATA_TYPE gradient_x, gradient_y;
 
  // horizontal gradient
  gradient_x = pixel_gradient_x(image, image_data, x, y);

  // vertical gradient
  gradient_y = pixel_gradient_y(image, image_data, x, y);

  return ((PIXEL_DATA_TYPE)sqrt((gradient_x * gradient_x) + (gradient_y * gradient_y)));
}

static PIXEL_DATA_TYPE pixel_gradient_x(const image_type* image,
                                        image_data_type image_data, int x, int y)
{
  static PIXEL_DATA_TYPE gradient_x;

  gradient_x = 0;
  gradient_x += pixel(image, image_data, (x - 1), (y - 1))[0];
  gradient_x += (2 * pixel(image, image_data, (x - 1), y)[0]);
  gradient_x += pixel(image, image_data, (x - 1), (y - 1))[0];
  gradient_x -= pixel(image, image_data, (x + 1), (y - 1))[0];
  gradient_x -= (2 * pixel(image, image_data, (x + 1), y)[0]);
  gradient_x -= pixel(image, image_data, (x + 1), (y - 1))[0];

  return gradient_x;
}

static PIXEL_DATA_TYPE pixel_gradient_y(const image_type* image,
                                        image_data_type image_data, int x, int y)
{
  static PIXEL_DATA_TYPE gradient_y;

  gradient_y = 0;
  gradient_y += pixel(image, image_data, (x - 1), (y + 1))[0];
  gradient_y += (2 * pixel(image, image_data, x, (y + 1))[0]);
  gradient_y += pixel(image, image_data, (x + 1), (y + 1))[0];
  gradient_y -= pixel(image, image_data, (x - 1), (y - 1))[0];
  gradient_y -= (2 * pixel(image, image_data, x, (y - 1))[0]);
  gradient_y -= pixel(image, image_data, (x + 1), (y - 1))[0];

  return gradient_y;
}
